3. Constraining Serialization
A generic class that has generic type parameters as members can still be marked for serialization:
[Serializable]
public class MyClass<T>
{
T m_T;
}
However, in such cases the generic class is
serializable only if the generic type parameter specified is
serializable. Consider this code:
public class SomeClass
{}
MyClass<SomeClass> obj;
obj is not serializable, because the type parameter SomeClass is not serializable. Consequently, MyClass<T>
may or may not be serializable, depending on the generic type parameter
used. This may result in a run-time loss of data or system corruption,
because the client application may not be able to persist the state of
the object.
To make things even worse, the type parameter
itself might be a generic type, whose own type parameters might not be
serializable, and so on:
[Serializable]
public class MyClass<T>
{
T m_T;
}
[Serializable]
public class SomeClass<T>
{}
public class SomeOtherClass
{}
//Will not work:
MyClass<SomeClass<SomeOtherClass>> obj;
Presently, .NET does not provide a mechanism for
constraining a generic type parameter to be serializable. However, there
are three workarounds to guarantee deterministic serialization
behavior. The first is to mark all member variables of the generic type
parameter as non-serializable:
[Serializable]
public class MyClass<T>
{
[NonSerialized]
T m_T;
}
Of course, this may seriously damage the generic class MyClass<T>'s
ability to function properly, in the case where you do need to
serialize the state of members of a generic type. The second workaround
is to place a constraint on the generic type parameter to implement ISerializable:
[Serializable]
public class MyClass<T> where T : ISerializable
{
T m_T;
}
This ensures that all instances of MyClass<T>,
regardless of the type parameter, are serializable, but it places the
burden of implementing custom serialization on all generic type
parameters used. The third and best solution is to perform a single
runtime check before any use of the type MyClass<T> and
abort the use immediately, before any damage can take place. The trick
is to place the runtime verification in the C# static constructor. Example 5 demonstrates this technique.
Example 5. Runtime enforcement of generic type parameter serialization
[Serializable]
class MyClass<T>
{
T m_T;
static MyClass()
{
SerializationUtil.ConstrainType(typeof(T));
}
}
public static class SerializationUtil
{
public static void ConstrainType(Type type)
{
bool serializable = type.IsSerializable;
if(serializable == false)
{
string message = "The type " + type + " is not serializable";
throw new SerializationException(message);
}
bool genericType = type.IsGenericType;
if(genericType)
{
Type[] typeArguments = type.GetGenericArguments();
Debug.Assert(typeArguments.Length >= 1);
Array.ForEach(typeArguments,ConstrainType);
}
}
//Rest of SerializationUtil
}
|
The C# static constructor is invoked exactly once
per type per app domain, upon the first attempt to instantiate an
object of that type. In Example 5, the static constructor calls the static helper method ConstrainType() of SerializationUtil. ConstrainType() then verifies that the specified type is serializable by checking the IsSerializable property of the type. If the type is not serializable, ConstrainType() throws a SerializationException, thus aborting any attempt to use the type.
To deal with the issue of having generic types as type parameters, ConstrainType
checks if the type in question is a generic type. If so, it obtains an
array of all its type parameters, and recursively calls itself verifying
that each type parameter down the declaration chain is serializable.
Performing the constraint verification in the
static constructor is a technique applicable to any constraint that you
cannot enforce at compile time yet have some programmatic way of
determining and enforcing at runtime. |